Las características clave de cualquier API son la estructura de las solicitudes y la estructura de las respuestas. Una solicitud HTTP consta de las siguientes partes:
GET, POST, DELETE, etc.)Un paquete de API debe ser capaz de generar estos componentes para realizar la llamada API deseada, que normalmente implicará algún tipo de autenticación.
Por ejemplo, para solicitar que la API de GitHub proporcione una lista de todos los problemas para el repositorio httr, enviamos una solicitud HTTP que se ve así:
-> GET /repos/hadley/httr HTTP/1.1
-> Host: api.github.com
-> Accept: application/vnd.github.v3+json
Aquí estamos utilizando una solicitud GET para el host api.github.com. La URL es /repos/hadley/httr, y enviamos un encabezado de aceptación que le dice a GitHub qué tipo de datos queremos.
En respuesta a esta solicitud, la API devolverá una respuesta HTTP que incluye:
Un cliente API necesita analizar estas respuestas, convertir los errores API en errores de R y devolver un objeto útil al usuario final. Para la solicitud HTTP anterior, GitHub devuelve:
<- HTTP/1.1 200 OK
<- Server: GitHub.com
<- Content-Type: application/json; charset=utf-8
<- X-RateLimit-Limit: 5000
<- X-RateLimit-Remaining: 4998
<- X-RateLimit-Reset: 1459554901
<-
<- {
<- "id": 2756403,
<- "name": "httr",
<- "full_name": "hadley/httr",
<- "owner": {
<- "login": "hadley",
<- "id": 4196,
<- "avatar_url": "https://avatars.githubusercontent.com/u/4196?v=3",
<- ...
<- },
<- "private": false,
<- "html_url": "https://github.com/hadley/httr",
<- "description": "httr: a friendly http package for R",
<- "fork": false,
<- "url": "https://api.github.com/repos/hadley/httr",
<- ...
<- "network_count": 1368,
<- "subscribers_count": 64
<- }
En primer lugar, encontramos un endpoint que no requiera autenticación: esto nos permite obtener los conceptos básicos funcionando antes de abordar las complejidades de la autenticación. Para este ejemplo, usaremos la lista de problemas httr que requiere que enviemos una solicitud GET a repos/hadley/httr:
library(httr)
github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
GET(url)
}
resp <- github_api("/repos/hadley/httr")
resp## Response [https://api.github.com/repositories/2756403]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json; charset=utf-8
## Size: 6.01 kB
## {
## "id": 2756403,
## "node_id": "MDEwOlJlcG9zaXRvcnkyNzU2NDAz",
## "name": "httr",
## "full_name": "r-lib/httr",
## "owner": {
## "login": "r-lib",
## "id": 22618716,
## "node_id": "MDEyOk9yZ2FuaXphdGlvbjIyNjE4NzE2",
## "avatar_url": "https://avatars0.githubusercontent.com/u/22618716?v=4",
## ...
A continuación, debemos tomar la respuesta devuelta por la API y convertirla en un objeto útil. Cualquier API devolverá una respuesta HTTP que consta de encabezados y un cuerpo. Si bien la respuesta puede venir en múltiples formas, dos de los formatos estructurados más comunes son XML y JSON.
Notemos que, si bien la mayoría de las API devolverán solo una u otra, algunas, como la API de amantes del color, nos permiten elegir cuál de ellas con un parámetro url:
GET("http://www.colourlovers.com/api/color/6B4106?format=xml")## Response [http://www.colourlovers.com/api/color/6B4106?format=xml]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: text/xml; charset=utf-8
## Size: 1.79 kB
## <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
## <colors numResults="1" totalResults="9933179">
## <color>
## <id>903893</id>
## <title><![CDATA[wet dirt]]></title>
## <userName><![CDATA[jessicabrown]]></userName>
## <numViews>521</numViews>
## <numVotes>1</numVotes>
## <numComments>0</numComments>
## <numHearts>0</numHearts>
## ...
GET("http://www.colourlovers.com/api/color/6B4106?format=json")## Response [http://www.colourlovers.com/api/color/6B4106?format=json]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json; charset=utf-8
## Size: 1.43 kB
Otros usan la negociación de contenido para determinar qué tipo de datos enviar. Si la API que estamos consultando hace esto, debemos incluir accept_json() o accept_xml() en nuestra solicitud.
Si hay opción, elegimos json: por lo general, es mucho más fácil trabajar que con xml.
La mayoría de las API devolverá la mayoría o toda la información útil en el cuerpo de respuesta, a la que se puede acceder utilizando content(). Para determinar qué tipo de información se devuelve, podemos usar http_type()
http_type(resp)## [1] "application/json"
Es recomendable verificar que el tipo sea el que esperamos en nuestra función auxiliar. Esto asegurará que recibamos un mensaje de error claro si la API cambia:
github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
resp <- GET(url)
if (http_type(resp) != "application/json") {
stop("API did not return json", call. = FALSE)
}
resp
}Algunas API mal redactadas dirán que el contenido es de tipo A, pero en realidad será de tipo B. En este caso, debemos presentar una queja ante los autores de la API, y hasta que solucionen el problema, simplemente ignoramos la verificación del tipo de contenido.
A continuación, tenemos que parsear el resultado en un objeto R. httr proporciona algunos parseos predeterminados, content(..., as = "auto") pero no es recomendable usarlos. En su lugar, es mejor parsearlos nosotros mismos explícitamente:
jsonlite o rjson.xml2.github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
resp <- GET(url)
if (http_type(resp) != "application/json") {
stop("API did not return json", call. = FALSE)
}
jsonlite::fromJSON(content(resp, "text"), simplifyVector = FALSE)
}En lugar de simplemente devolver la respuesta como una lista, es una buena práctica crear un objeto S3 simple. De esta forma, podemos devolver la respuesta y el objeto parseado, y proporcionar un buen método de impresión. Esto hará que la depuración más tarde sea mucho más agradable.
github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
resp <- GET(url)
if (http_type(resp) != "application/json") {
stop("API did not return json", call. = FALSE)
}
parsed <- jsonlite::fromJSON(content(resp, "text"), simplifyVector = FALSE)
structure(
list(
content = parsed,
path = path,
response = resp
),
class = "github_api"
)
}
print.github_api <- function(x, ...) {
cat("<GitHub ", x$path, ">\n", sep = "")
str(x$content)
invisible(x)
}
github_api("/users/hadley")## <GitHub /users/hadley>
## List of 31
## $ login : chr "hadley"
## $ id : int 4196
## $ node_id : chr "MDQ6VXNlcjQxOTY="
## $ avatar_url : chr "https://avatars3.githubusercontent.com/u/4196?v=4"
## $ gravatar_id : chr ""
## $ url : chr "https://api.github.com/users/hadley"
## $ html_url : chr "https://github.com/hadley"
## $ followers_url : chr "https://api.github.com/users/hadley/followers"
## $ following_url : chr "https://api.github.com/users/hadley/following{/other_user}"
## $ gists_url : chr "https://api.github.com/users/hadley/gists{/gist_id}"
## $ starred_url : chr "https://api.github.com/users/hadley/starred{/owner}{/repo}"
## $ subscriptions_url : chr "https://api.github.com/users/hadley/subscriptions"
## $ organizations_url : chr "https://api.github.com/users/hadley/orgs"
## $ repos_url : chr "https://api.github.com/users/hadley/repos"
## $ events_url : chr "https://api.github.com/users/hadley/events{/privacy}"
## $ received_events_url: chr "https://api.github.com/users/hadley/received_events"
## $ type : chr "User"
## $ site_admin : logi FALSE
## $ name : chr "Hadley Wickham"
## $ company : chr "@rstudio "
## $ blog : chr "http://hadley.nz"
## $ location : chr "Houston, TX"
## $ email : NULL
## $ hireable : NULL
## $ bio : chr "Chief Scientist at @RStudio"
## $ public_repos : int 212
## $ public_gists : int 163
## $ followers : int 13676
## $ following : int 6
## $ created_at : chr "2008-04-01T14:47:36Z"
## $ updated_at : chr "2018-07-23T16:32:52Z"
La API puede devolver datos no válidos, pero esto debería ser raro, por lo que podemos confiar en que el parseador proporcione un mensaje de error útil.
A continuación, debemos asegurarnos de que el API arroje un error si la solicitud falla. El uso de una API web introduce puntos de falla adicionales posibles en el código R además de los que ocurren en R mismo. Éstas incluyen:
Debemos asegurarnos de que todos se conviertan en errores R regulares. Podemos averiguar si hay un problema con http_error(), que verifica el código de estado HTTP. Los códigos de estado en el rango 400 generalmente significan que hemos hecho algo mal. Los códigos de estado en el rango de 500 normalmente significan que algo ha ido mal del lado del servidor.
A menudo, la API proporcionará información sobre el error en el cuerpo de la respuesta: debemos usar esto cuando esté disponible. Si la API devuelve errores especiales para problemas comunes, es posible que deseemos proporcionar más detalles sobre el error. Por ejemplo, si se nos agotan las solicitudes, es posible que deseemos decirle al usuario cuánto tiempo debe esperar hasta que pueda realizar la siguiente solicitud.
github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
resp <- GET(url)
if (http_type(resp) != "application/json") {
stop("API did not return json", call. = FALSE)
}
parsed <- jsonlite::fromJSON(content(resp, "text"), simplifyVector = FALSE)
if (http_error(resp)) {
stop(
sprintf(
"GitHub API request failed [%s]\n%s\n<%s>",
status_code(resp),
parsed$message,
parsed$documentation_url
),
call. = FALSE
)
}
structure(
list(
content = parsed,
path = path,
response = resp
),
class = "github_api"
)
}
github_api("/user/hadley")
#> Error: GitHub API request failed [404]
#> Not Found
#> <https://developer.github.com/v3>Algunas API mal redactadas arrojarán diferentes tipos de respuesta en función de si la solicitud tuvo éxito o no. Si nuestra API hace esto, debemos verificar la función de solicitud status_code() antes de parsear la respuesta.
Mientras estamos en esta función, hay un encabezado importante que debemos establecer para cada contenedor de API: el agente de usuario. El agente de usuario es una cadena utilizada para identificar al cliente. Esto es más útil para el propietario de la API, ya que les permite ver quién está usasndola. Si estamos consultando una API comercial, esto facilita que los defensores internos de R vean cuántas personas usan su API a través de R y, con suerte, asignen más recursos.
Un buen valor predeterminado para un contenedor de API en R es convertirlo en la URL de nuestro repositorio de GitHub:
ua <- user_agent("http://github.com/hadley/httr")
ua
#> <request>
#> Options:
#> * useragent: http://github.com/hadley/httr
github_api <- function(path) {
url <- modify_url("https://api.github.com", path = path)
resp <- GET(url, ua)
if (http_type(resp) != "application/json") {
stop("API did not return json", call. = FALSE)
}
parsed <- jsonlite::fromJSON(content(resp, "text"), simplifyVector = FALSE)
if (status_code(resp) != 200) {
stop(
sprintf(
"GitHub API request failed [%s]\n%s\n<%s>",
status_code(resp),
parsed$message,
parsed$documentation_url
),
call. = FALSE
)
}
structure(
list(
content = parsed,
path = path,
response = resp
),
class = "github_api"
)
}La mayoría de las API funcionan al ejecutar un método HTTP en una URL específica con algunos parámetros adicionales. Estos parámetros se pueden especificar de varias maneras, incluso en la ruta URL, en los argumentos de consulta URL, en los encabezados HTTP y en el cuerpo de la solicitud en sí. Estos parámetros se pueden controlar usando funciones de httr:
modify_url()query para GET(), POST(), etc.add_headers()body para GET(), POST(), etc.Las API RESTful también usan el verbo HTTP para comunicar argumentos (por ejemplo, GET recupera un archivo, POST agrega un archivo, DELETE elimina un archivo, etc.). Podemos usar el servicio httpbin para mostrar cómo enviar argumentos en cada una de estas formas.
# modify_url
POST(modify_url("https://httpbin.org", path = "/post"))## Response [https://httpbin.org/post]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json
## Size: 421 B
## {
## "args": {},
## "data": "",
## "files": {},
## "form": {},
## "headers": {
## "Accept": "application/json, text/xml, application/xml, */*",
## "Accept-Encoding": "gzip, deflate",
## "Connection": "close",
## "Content-Length": "0",
## ...
# query arguments
POST("http://httpbin.org/post", query = list(foo = "bar"))## Response [http://httpbin.org/post?foo=bar]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json
## Size: 448 B
## {
## "args": {
## "foo": "bar"
## },
## "data": "",
## "files": {},
## "form": {},
## "headers": {
## "Accept": "application/json, text/xml, application/xml, */*",
## "Accept-Encoding": "gzip, deflate",
## ...
# headers
POST("http://httpbin.org/post", add_headers(foo = "bar"))## Response [http://httpbin.org/post]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json
## Size: 439 B
## {
## "args": {},
## "data": "",
## "files": {},
## "form": {},
## "headers": {
## "Accept": "application/json, text/xml, application/xml, */*",
## "Accept-Encoding": "gzip, deflate",
## "Connection": "close",
## "Content-Length": "0",
## ...
# body
## as form
POST("http://httpbin.org/post", body = list(foo = "bar"), encode = "form")## Response [http://httpbin.org/post]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json
## Size: 498 B
## {
## "args": {},
## "data": "",
## "files": {},
## "form": {
## "foo": "bar"
## },
## "headers": {
## "Accept": "application/json, text/xml, application/xml, */*",
## "Accept-Encoding": "gzip, deflate",
## ...
## as json
POST("http://httpbin.org/post", body = list(foo = "bar"), encode = "json")## Response [http://httpbin.org/post]
## Date: 2018-07-28 15:58
## Status: 200
## Content-Type: application/json
## Size: 497 B
## {
## "args": {},
## "data": "{\"foo\":\"bar\"}",
## "files": {},
## "form": {},
## "headers": {
## "Accept": "application/json, text/xml, application/xml, */*",
## "Accept-Encoding": "gzip, deflate",
## "Connection": "close",
## "Content-Length": "13",
## ...
Muchas API utilizarán solo una de estas formas de aprobación de argumentos, pero otras utilizarán varias de ellas en combinación. La mejor práctica es aislar al usuario de cómo y dónde los diferentes argumentos son utilizados por la API y en su lugar simplemente exponer los argumentos relevantes a través de los argumentos de la función R, algunos de los cuales podrían usarse en la URL, en los encabezados, en el cuerpo, etc.
Si un parámetro tiene un pequeño conjunto fijo de valores posibles permitidos por la API, podemos usar la lista en los argumentos predeterminados y luego utilizar match.arg() para asegurarse de que la persona que llama solo proporciona uno de esos valores. (Esto también permite al usuario proporcionar los prefijos únicos cortos).
f <- function(x = c("apple", "banana", "orange")) {
match.arg(x)
}
f("a")## [1] "apple"
Es una buena práctica establecer explícitamente los valores predeterminados para los argumentos que no son necesarios como NULL. Si hay un valor predeterminado, debe ser el primero que figura en el vector de argumentos permitidos.
Se pueden consultar muchas API sin autenticación (como si las llamaras en un navegador web). Sin embargo, otros requieren autenticación para realizar solicitudes particulares o para evitar límites de velocidad y otras limitaciones:
Autenticación “básica”: Esta requiere un nombre de usuario y contraseña (o algunas veces solo un nombre de usuario). Esto se pasa como parte de la solicitud HTTP. En httr, podemos hacer: GET("http://httpbin.org", authenticate("username", "password"))
Autenticación básica con una llave API: una alternativa proporcionada por muchas API es una “llave” o “token” que se pasa como parte de la solicitud. Es mejor que una combinación de nombre de usuario / contraseña porque puede regenerarse independientemente del nombre de usuario y la contraseña.
Esta llave API se puede especificar de varias formas diferentes: en un argumento de consulta URL, en un encabezado HTTP como el encabezado Authorization o en un argumento dentro del cuerpo de la solicitud.
oauth1.0_token() para obtener más información.) El estándar actual de OAuth 2.0 es muy común en las aplicaciones web modernas. Implica un viaje de ida y vuelta entre el cliente y el servidor para establecer si el cliente API tiene la autoridad para acceder a los datos. Ver oauth2.0_token(). Está bien publicar el ID de la aplicación y el “secreto” de la aplicación, estos no son realmente importantes para la seguridad de los datos del usuario.Algunas API describen sus procesos de autenticación de forma imprecisa, por lo que debe tenerse cuidado para comprender el verdadero mecanismo de autenticación independientemente de la etiqueta utilizada en los documentos de la API.
Es posible especificar la(s) llave(s) o token(s) requeridas para la autenticación básica o de OAuth de diferentes maneras. También es posible que necesitemos alguna forma de preservar las credenciales del usuario entre llamadas a funciones para que los usuarios finales no necesiten especificarlas cada vez. Un buen comienzo es usar una variable de entorno. Aquí hay un ejemplo de cómo escribir una función que comprueba la presencia de un token de acceso personal de GitHub y, de lo contrario, se producen errores.
github_pat <- function() {
pat <- Sys.getenv('GITHUB_PAT')
if (identical(pat, "")) {
stop("Please set env var GITHUB_PAT to your github personal access token",
call. = FALSE)
}
pat
}Muchas API tienen una tasa límite, lo que significa que solo podemos enviar una determinada cantidad de solicitudes por hora. A menudo, si nuestra solicitud está limitada, el mensaje de error nos dirá cuánto tiempo debemos esperar antes de realizar otra solicitud. Es posible que deseemos exponer esto al usuario, o incluso incluir un muro Sys.sleep() que espere lo suficiente.
Por ejemplo, podríamos implementar una función rate_limit() que nos indique cuántas llamadas contra la API de Github están disponibles para nosotros.
rate_limit <- function() {
github_api("/rate_limit")
}
rate_limit()## <GitHub /rate_limit>
## List of 2
## $ resources:List of 3
## ..$ core :List of 3
## .. ..$ limit : int 60
## .. ..$ remaining: int 57
## .. ..$ reset : int 1532797087
## ..$ search :List of 3
## .. ..$ limit : int 10
## .. ..$ remaining: int 10
## .. ..$ reset : int 1532793550
## ..$ graphql:List of 3
## .. ..$ limit : int 0
## .. ..$ remaining: int 0
## .. ..$ reset : int 1532797090
## $ rate :List of 3
## ..$ limit : int 60
## ..$ remaining: int 57
## ..$ reset : int 1532797087
Después de que la primera versión funcione, a menudo queremos pulir la salida para que sea más fácil de usar. Para este ejemplo, podemos analizar las marcas de tiempo de Unix en tipos de fecha más útiles.
rate_limit <- function() {
req <- github_api("/rate_limit")
core <- req$content$resources$core
reset <- as.POSIXct(core$reset, origin = "1970-01-01")
cat(core$remaining, " / ", core$limit,
" (Resets at ", strftime(reset, "%H:%M:%S"), ")\n", sep = "")
}
rate_limit()## 57 / 60 (Resets at 11:58:07)
Los servicios web que proporcionan información a través de API suelen proporcionarla en formato JSON, XML o ambos. Los dos formatos son similares: organizan la información en forma de árbol.
Por ejemplo, el INE (de España) proporciona un API JSON del que se puede bajar información de interés estadístico. Tiene, además, un servicio que permite construir la consulta, i.e., obtener la URL con la cual consultar una serie de datos en concreto. Usándola, encontramos que para obtener la población de cada provincia española por sexos durante los últimos cinco años tenemos que consultarla.
Pero podemos realizar la consulta de la siguiente manera:
library(rjson)
pob <- readLines("http://servicios.ine.es/wstempus/js/ES/DATOS_TABLA/2852?nult=5&tip=AM")
pob <- paste(pob, collapse = " ")
pob <- fromJSON(pob)Leemos la URL y colapsamos todas las líneas en una única cadena de texto, una exigencia de fromJSON. Esta es la función que llamamos en última instancia para convertir el JSON en una estructura de R, una lista que contiene, a su vez, otras listas.
class(pob)## [1] "list"
length(pob)## [1] 159
Cada uno de estos elementos tiene una serie de atributos y una sublista que almacena los datos anuales. Entonces,
pob[[89]]$Data[[5]]$Valor## [1] 168013
es el valor (es decir, la población) correspondiente al quinto periodo (o año) del elemento 85 de la primera lista.
Idealmente, queremos nuestros datos (o la parte más relevante de ellos) en un data frame para procesarlo como ya sabemos.
Otras APIs proporcionan información en formato XML. Por ejemplo, la del Banco Mundial:
library(xml2)
bm <- read_xml("http://api.worldbank.org/countries/all/indicators/NY.GDP.MKTP.CD?date=2009:2010&per_page=500&page=1")
mex <- xml_find_all(bm, "//*/wb:data[wb:country[@id='MX']]/wb:value")
as.numeric(xml_text(mex))## [1] 1.057801e+12 9.000454e+11
El código anterior proporciona el PIB de México de los años 2009 y 2010 en dólares. La función read_xml() lee una URL de la API del Banco Mundial que extrae el indicador NY.GDP.MKTP.CD (PIB según la documentación de la API) y la procesa. A diferencia de lo que ocurría con JSON, el objeto bm no es una lista de R (aunque la podemos convertir en una usando la función as_list()), sino un objeto de la clase xml_document.
El objeto mex contiene dos nodos y la función xml_text() permite extraer su contenido (en formato de texto, que tenemos que convertir en números). En general, los nodos tienen atributos y el texto es uno más de ellos.
Veremos, a continuación, algunos ejemplos de API HTTP públicas que publican datos en formato JSON. Estos son excelentes para tener una idea de las estructuras complejas que se encuentran en los datos JSON del mundo real. Todos los servicios son gratuitos, pero algunos requieren registro / autenticación.
Usaremos la librería jsonlite para leer los resultados:
library(jsonlite)Github tiene API para obtener datos en vivo en casi todas las actividades. A continuación algunos ejemplos de un conocido paquete de R y su autor:
hadley_orgs <- fromJSON("https://api.github.com/users/hadley/orgs")
hadley_repos <- fromJSON("https://api.github.com/users/hadley/repos")
gg_commits <- fromJSON("https://api.github.com/repos/hadley/ggplot2/commits")
gg_issues <- fromJSON("https://api.github.com/repos/hadley/ggplot2/issues")
# issues recientes
paste(format(gg_issues$user$login), ":", gg_issues$title)## [1] "clauswilke : geom_label() can't rotate text & gets descent heights wrong"
## [2] "clauswilke : Enable user-defined theme elements by making element tree part of the theme."
## [3] "karawoo : preserve = \"single\" has no effect for continuous axes"
## [4] "gregrs-uk : Clarify geom_label documentation"
## [5] "ilarischeinin : Add new parameter \"preset\" for ggsave() to specify width/height at once"
## [6] "ptoche : geom_area position_stack order appears to have changed"
## [7] "dpseidel : Inconsistent behaviour with geom_boxplot `width` "
## [8] "MJochim : Documentation of stat_ellipse() is misleading"
## [9] "woodwards : scale_x_datetime() limits not working when only one x value "
## [10] "paleolimbot : Facet labels on the left are not clipped, but all others are"
## [11] "dpseidel : Minor Documentation Fixes"
## [12] "aosmith16 : position_dodge2() works without a grouping variable in a layer"
## [13] "brodieG : Apparent Inconsistency Between `Facet$setup_data` and `Facet$finish_data` usage"
## [14] "bpbraun : Strip placement default should be at left and outside of axis"
## [15] "yutannihilation : [WIP] Implement geom_sf_label() and geom_sf_text()"
## [16] "clauswilke : Don't create a new graphics device on exit in ggsave(). Closes #2363."
## [17] "baderstine : geom_text throws error when angle = NA"
## [18] "pitakakariki : Circular error message - geom_bar, width, and stat-bin."
## [19] "dpseidel : Fix cryptic error messages caused by missing aesthetics"
## [20] "itcarroll : feature request to provide faceting via aesthetics"
## [21] "dpseidel : Allow default geom aesthetics to be set from theme"
## [22] "msberends : Use markdown for italic/bold in titles"
## [23] "yutannihilation : Stat for ploting sf data as label and text"
## [24] "lorenzwalthert : Applying tidyverse style guide with styler"
## [25] "Eluvias : Different alignment of legend labels when removing the legend title"
## [26] "hadley : Move ggplot2 website to gh-pages branch"
## [27] "hadley : Update news links on ggplot2.tidyverse.org"
## [28] "lawremi : ggplot_add.by()"
## [29] "jpasquier : position_nudge does not work with geom_boxplot"
## [30] "thomasp85 : Move call to st_transform to transform"
Una única API pública que muestra la ubicación, el estado y la disponibilidad actual de todas las estaciones en el sistema de bicicletas compartidas de la ciudad de Nueva York.
citibike <- fromJSON("http://citibikenyc.com/stations/json")
stations <- citibike$stationBeanList
colnames(stations)## [1] "id" "stationName"
## [3] "availableDocks" "totalDocks"
## [5] "latitude" "longitude"
## [7] "statusValue" "statusKey"
## [9] "availableBikes" "stAddress1"
## [11] "stAddress2" "city"
## [13] "postalCode" "location"
## [15] "altitude" "testStation"
## [17] "lastCommunicationTime" "landMark"
nrow(stations)## [1] 815
Ergast Developer API es un servicio web experimental que proporciona un registro histórico de datos de carreras de automóviles para fines no comerciales.
res <- fromJSON('http://ergast.com/api/f1/2004/1/results.json')
drivers <- res$MRData$RaceTable$Races$Results[[1]]$Driver
colnames(drivers)## [1] "driverId" "code" "url" "givenName"
## [5] "familyName" "dateOfBirth" "nationality" "permanentNumber"
drivers[1:10, c("givenName", "familyName", "code", "nationality")]A continuación un ejemplo de la API de ProPublica Nonprofit Explorer donde recuperamos las primeras 10 páginas de organizaciones exentas de impuestos en los Estados Unidos ordenadas por ingresos. La función rbind_pages() se usa para combinar las páginas en un único set de datos.
# guardamos todas las páginas en una lista primero
baseurl <- "https://projects.propublica.org/nonprofits/api/v1/search.json?order=revenue&sort_order=desc"
pages <- list()
for(i in 0:10){
mydata <- fromJSON(paste0(baseurl, "&page=", i))
message("Retrieving page ", i)
pages[[i+1]] <- mydata$filings
}
# combinamos todas en una
filings <- rbind_pages(pages)
# revisamos la salida
nrow(filings)[1] 275
filings[1:10, c("organization.sub_name", "organization.city", "totrevenue")] organization.sub_name organization.city totrevenue
1 KAISER FOUNDATION HEALTH PLAN INC OAKLAND 40148558254
2 KAISER FOUNDATION HEALTH PLAN INC OAKLAND 37786011714
3 KAISER FOUNDATION HOSPITALS OAKLAND 20796549014
4 KAISER FOUNDATION HOSPITALS OAKLAND 17980030355
5 PARTNERS HEALTHCARE SYSTEM INC SOMERVILLE 10619215354
6 UPMC PITTSBURGH 10098163008
7 UAW RETIREE MEDICAL BENEFITS TR DETROIT 9890722789
8 THRIVENT FINANCIAL FOR LUTHERANS MINNEAPOLIS 9475129863
9 THRIVENT FINANCIAL FOR LUTHERANS MINNEAPOLIS 9021585970
10 DIGNITY HEALTH SAN FRANCISCO 8718896265
The New York Times tiene varias API como parte de la red de desarrolladores de NYT. Estas interfaces con datos de varios departamentos, como artículos de noticias, reseñas de libros, bienes raíces, etc. Se requiere registro (pero gratis) y se puede obtener una clave aquí. El código a continuación incluye algunas claves de ejemplo para fines ilustrativos.
# articulos
article_key <- "&api-key=b75da00e12d54774a2d362adddcc9bef"
url <- "http://api.nytimes.com/svc/search/v2/articlesearch.json?q=obamacare+socialism"
req <- fromJSON(paste0(url, article_key))
articles <- req$response$docs
colnames(articles)## [1] "web_url" "snippet" "print_page"
## [4] "blog" "source" "multimedia"
## [7] "headline" "keywords" "pub_date"
## [10] "document_type" "news_desk" "byline"
## [13] "type_of_material" "_id" "word_count"
## [16] "score" "uri"
# best sellers
books_key <- "&api-key=76363c9e70bc401bac1e6ad88b13bd1d"
url <- "http://api.nytimes.com/svc/books/v2/lists/overview.json?published_date=2013-01-01"
req <- fromJSON(paste0(url, books_key))
bestsellers <- req$results$list
category1 <- bestsellers[[1, "books"]]
subset(category1, select = c("author", "title", "publisher")) # reviews de películas
movie_key <- "&api-key=b75da00e12d54774a2d362adddcc9bef"
url <- "http://api.nytimes.com/svc/movies/v2/reviews/dvd-picks.json?order=by-date"
req <- fromJSON(paste0(url, movie_key))
reviews <- req$results
colnames(reviews)## [1] "display_title" "mpaa_rating" "critics_pick"
## [4] "byline" "headline" "summary_short"
## [7] "publication_date" "opening_date" "date_updated"
## [10] "link" "multimedia"
reviews[1:5, c("display_title", "byline", "mpaa_rating")]La Fundación Sunlight es una organización sin fines de lucro que ayuda a que el gobierno sea transparente y responsable a través de datos, herramientas, políticas y periodismo. Podemos registrar una llave gratis aquí.
key <- "&apikey=39c83d5a4acc42be993ee637e2e4ba3d"
# todo sobre drones
drone_bills <- fromJSON(paste0("http://openstates.org/api/v1/bills/?q=drone", key))
drone_bills$title <- substring(drone_bills$title, 1, 40)
print(drone_bills[1:5, c("title", "state", "chamber", "type")]) title state chamber type
1 AIRPORT AUTHORITIES-DRONES il upper bill
2 Study Drone Use By Public Safety Agencie co lower bill
3 AIRCRAFT/AVIATION: Provides for the exc la upper bill
4 relative to the use of drones. nh lower bill
5 Use or Operation of a Drone by Certain O fl lower bill
# legisladores locales
legislators <- fromJSON(paste0("http://congress.api.sunlightfoundation.com/",
"legislators/locate?latitude=42.96&longitude=-108.09", key))
subset(legislators$results, select=c("last_name", "chamber", "term_start", "twitter_id")) last_name chamber term_start twitter_id
1 Cheney house 2017-01-03 RepLizCheney
2 Enzi senate 2015-01-06 SenatorEnzi
3 Barrasso senate 2013-01-03 SenJohnBarrasso
La API de Twitter requiere autenticación OAuth2. Un código de ejemplo:
# creamos una llave en https://dev.twitter.com/apps
consumer_key = "EZRy5JzOH2QQmVAe9B4j2w";
consumer_secret = "OIDC4MdfZJ82nbwpZfoUO4WOLTYjoRhpHRAWj6JMec";
# usamos auth básico
secret <- jsonlite::base64_enc(paste(consumer_key, consumer_secret, sep = ":"))
req <- httr::POST("https://api.twitter.com/oauth2/token",
httr::add_headers(
"Authorization" = paste("Basic", gsub("\n", "", secret)),
"Content-Type" = "application/x-www-form-urlencoded;charset=UTF-8"
),
body = "grant_type=client_credentials"
);
# extraemos el token de acceso
httr::stop_for_status(req, "authenticate with twitter")
token <- paste("Bearer", httr::content(req)$access_token)
# llamada al API
url <- "https://api.twitter.com/1.1/statuses/user_timeline.json?count=10&screen_name=Rbloggers"
req <- httr::GET(url, httr::add_headers(Authorization = token))
json <- httr::content(req, as = "text")
tweets <- fromJSON(json)
substring(tweets$text, 1, 100)## [1] "Le Monde puzzle [#1062] https://t.co/gLreVhmw4B #rstats #DataScience"
## [2] "Hacking our way through UpSetR https://t.co/c8FcFcVdNc #rstats #DataScience"
## [3] "Weight loss in the U.S. – An analysis of NHANES data with tidyverse https://t.co/QOAAjPNp0u #rstats "
## [4] "aRt with code https://t.co/nYY5eru3ku #rstats #DataScience"
## [5] "CHAID v ranger v xgboost – a comparison – July 27, 2018 https://t.co/iqIhsOCheZ #rstats #DataScience"
## [6] "No worries! Afterthoughts from UseR 2018 https://t.co/rVnbofxDGy #rstats #DataScience"
## [7] "How to use Covariates to Improve your MaxDiff Model https://t.co/zdJawG81ft #rstats #DataScience"
## [8] "Announcing the 1st Bookdown Contest https://t.co/IiSCfc73GO #rstats #DataScience"
## [9] "Using themes in ggplot2 https://t.co/B0Gk95U6HZ #rstats #DataScience"
## [10] "Cucumber time, food on a 2D plate / plane https://t.co/rgX5N6StWh #rstats #DataScience"
Los autores quisieron trasladar la arquitectura de ggplot2 a otro ámbito: el de la representación de información georreferenciada. ggplot2 permite representar información geográfica (puntos, segmentos, etc.): basta con que las estéticas x y y se correspondan con la longitud y la latitud de los datos. Lo que permite hacer ggmap es, en esencia, añadir a los gráficos ya conocidos una capa cartográfica adicional. Para eso usa recursos disponibles en la web a través de APIs (de Google y otros).
Un ejemplo sencillo ilustra los usos de ggmap:
library(ggmap)Existen varios proveedores que proporcionan APIs de geolocalización. Uno de ellos es Google; dado el nombre más o menos normalizado de un lugar, la API de Google devuelve sus coordenadas. Este servicio tiene una versión gratuita que permite realizar un determinado número de consultas diarias; para usos más intensivos, es necesario adquirir una licencia. La función geocode encapsula la consulta a dicha API y devuelve un objeto (un data.frame) que contiene las coordenadas del lugar de interés:
unizar <- geocode('Calle de Pedro Cerbuna 12, Zaragoza, España',
source = "google")La función get_map consulta otro servicio de información cartográfica (GoogleMaps en el ejemplo siguiente) y descarga un mapa. La función exige una serie de argumentos: el nivel de zoom, si se quiere un mapa de carreteras o del terreno, etc. Son, de hecho, los parámetros que uno puede manipular con los controles de la interfaz habitual de GoogleMaps.
map.unizar <- get_map(location = as.numeric(unizar),
color = "color",
maptype = "roadmap",
scale = 2,
zoom = 16)Es obvio que para poder invocar las dos funciones anteriores hace falta una conexión a internet. Sin embargo, el resto de las operaciones que se van a realizar se ejecutan localmente. Se puede, por ejemplo, representar el mapa directamente (haciendo ggmap(map.unizar)). O bien se puede marcar sobre él el punto de interés:
ggmap(map.unizar) + geom_point(aes(x = lon, y = lat),
data = unizar, colour = 'red',
size = 4)Como veremos a continuación, no estamos limitados a representar un único punto: unizar podría ser una tabla con más de una fila y todos los puntos se representarían sobre el mapa.
Como puede apreciarse, la sintaxis es similar a la de ggplot2. Una diferencia notable es que, ahora, los datos se pasan en la capa, es decir, en este caso, en la función geom_point.
ggmap incluye muchas funciones que pueden clasificarse en tres categorías amplias:
Funciones para obtener mapas (de diversos tipos y de distintos orígenes: Google, Stamen, OpenStreetMap).
Funciones que utilizan APIs de Google y otros. Por ejemplo, geocode, revgeocode y route consultan la información que tienen distintos proveedores de servicios vía API sobre las coordenadas de un determinado lugar; indican el lugar al que se refieren unas coordenadas y, finalmente, encuentran rutas entre dos puntos. Es conveniente recordar que las consultas a los servicios de Google Maps exige la aceptación de las condiciones de uso y que existe un límite diario en el número de consultas gratuitas.
Funciones que pintan mapas y que representan determinados elementos adicionales (puntos, segmentos, etc.) en mapas.
ggmap obtiene sus mapas, por defecto, de GoogleMaps. Sin embargo hay otros proveedores de mapas libres, como OpenStreetMap (OSM) o Stamen. Cada proveedor exige una serie de parámetros distintos y, por ejemplo, un zoom de 8 puede significar una escala distinta en GoogleMaps que en OSM. Sin embargo, los autores de ggmap se han tomado la molestia de homogeneizar los argumentos de llamada para que sean aproximadamente equivalentes en todos los proveedores.
ggmap incluye funciones específicas para cada proveedor, como get_googlemap o get_stamenmap, pero salvo para usos avanzados, es recomendable usar la función get_map, que ofrece un punto de entrada único y homogéneo.
La siguiente imagen muestra cuatro mapas obtenidos de diversos proveedores y con diversas opciones. En la fila superior, una capa de Google en modo imagen de satélite y otra estándar. En la inferior, dos mapas de Stamen, uno en modo toner y otro en modo watercolor o acuarela. Son solo cuatro de los muchos a los que la función get_map puede acceder.
Muchos servicios de información cartográfica proporcionan API para realizar consultas. Las API se consultan, típicamente, con URLs convenientemente construidas. Por ejemplo, la URL
http://maps.googleapis.com/maps/api/geocode/json?address=Universidad+de+Zaragoza
consulta el servicio de geolocalización de GoogleMaps y devuelve las coordenadas de la Universidad de Zaragoza (así como otra información relevante en formato JSON). La función geolocate de ggmap facilita la consulta a dicho servicio: toma su argumento (el nombre de un lugar), construye internamente la URL, realiza la consulta (para lo que es necesario conexión a internet), lee la respuesta y le da un formato conveniente (en este caso, un data.frame de R).
La de geolocalización no es la única API que permite consultar ggmap. También permite invertir la geolocalización, es decir, dadas unas coordenadas, devolver el nombre del lugar al que se refieren:
revgeocode(as.numeric(unizar))## [1] "Calle de Pedro Cerbuna, 12, 50009 Zaragoza, Spain"
Finalmente, route permite obtener la ruta entre dos puntos distintos:
mapa <- get_map("Madrid", source = "stamen", maptype = "toner", zoom = 12)
ruta <- route(from = "Puerta del Sol, Madrid", to = "Plaza de Castilla, Madrid")
ggmap(mapa) +
geom_path(aes(x = startLon, y = startLat, xend = endLon, yend = endLat),
colour = "red", size = 2, data = ruta)En el mapa anterior la ruta elegida por GoogleMaps para ir de la Puerta del Sol hasta la plaza de Castilla (dos plazas de Madrid) está marcado en rojo sobre un mapa de Stamen de tipo toner.
Como se ha visto en las secciones anteriores, la función ggmap permite representar un mapa descargado previamente. Además, a esa capa subyacente se le pueden añadir elementos (puntos, segmentos, densidades, etc.) usando las funciones ya conocidas de ggplot2: geom_point, etc.
En ggplot2 existe una función, geom_path que dibuja caminos (secuencias de segmentos). Se puede utilizar en ggmap para dibujar rutas, aunque este paquete proporciona una función especial, geom_leg que tiene la misma finalidad aunque con algunas diferencias menores: por ejemplo, los segmentos tienen las puntas redondeadas, para mejorar el resultado gráfico.
En los ejemplos que siguen se va a utilizar el conjunto de datos crimes que forma parte del paquete ggmap y que incluye información geolocalizada de crímenes cometidos en la ciudad de Houston. En realidad, solo consideraremos los crímenes serios, es decir,
crimes.houston <- subset(crime, ! crime$offense %in% c("auto theft", "theft", "burglary"))El tipo de mapas más simples son los que se limitan a representar puntos sobre una capa cartográfica.
HoustonMap <- qmap("houston", zoom = 14, color = "bw")
HoustonMap +
geom_point(aes(x = lon, y = lat, colour = offense), data = crimes.houston, size = 1)En el código anterior hemos usado la función qmap, una función auxiliar que internamente llama primero get_map y luego a ggmap.
Los mecanismos conocidos de ggplot2, como las facetas, están disponibles en ggmap: es posible crear una retícula de mapas usando facet_wrap. En este primer caso, descomponiendo el gráfico anterior por tipo de crimen.
HoustonMap +
geom_point(aes(x = lon, y = lat), data = crimes.houston, size = 1) +
facet_wrap(~ offense)O, alternativamente, por día de la semana.
HoustonMap +
geom_point(aes(x = lon, y = lat), data = crimes.houston, size = 1) +
facet_wrap(~ day)Representaremos información geográfica contenida en ficheros .kml usando ggmap como ejemplo de la versatilidad del paquete.
library(maptools)
# un dataset bajado del Ayto. de Madrid
rutas <- getKMLcoordinates("data/130111_vias_ciclistas.kml")El conjunto de datos anterior contiene una lista de rutas (inspecciónalo), que queremos convertir en una única tabla para poder representarlas gráficamente con ggmap.
library(plyr)
rutas <- ldply(1:length(rutas), function(x) data.frame(rutas[[x]], id = x))
mapa <- get_map("Madrid", source = "stamen", maptype = "toner", zoom = 12)
ggmap(mapa) + geom_path(aes(x = X1, y = X2, group = id), data = rutas, colour = "red")Además de geom_point, también están disponibles otros tipos de capas de ggplot2, como stat_bin2d, que cuenta el número de eventos que suceden en regiones cuadradas de un tamaño predefinido.
HoustonMap +
stat_bin2d(
aes(x = lon, y = lat, colour = offense, fill = offense),
size = .5, bins = 30, alpha = 1/2,
data = crimes.houston
)O se puede usar stat_density2d, que representa intensidades, para identificar las zonas de mayor criminalidad.
HoustonMap +
stat_density2d(aes(x = lon, y = lat, fill = ..level.., alpha = ..level..),
size = 2, data = crimes.houston,
geom = "polygon"
)Más sobre ggmap en este artículo.